package org.simpleframework.util;

import lombok.extern.slf4j.Slf4j;
import org.simpleframework.consts.FrameWorkConst;

import java.io.File;
import java.io.FileFilter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;

@Slf4j
public class ClassUtil {
    /**
     * 获取包下类集合
     *
     * @parampackageName包名
     * @return 类集合
     */
    public static Set<Class<?>> extractPackageClasses(String packageName) {
        // 1) 获得类加载器:
        ClassLoader classLoader = getClassLoader();
        // 2) 通过类加载器加载指定路径的资源,获得其对应的绝对URL
        URL url = classLoader.getResource(packageName.replace(".","/"));
        if(url == null) {
            log.warn(FrameWorkConst.PACKAGE_NOT_FOUND_ERROR_MSG + packageName);
            return  null;
        }
        // 3) 依据不同的资源类型，采用不同的方式获取资源的集合
        Set<Class<?>> classSet = null;
        // 过滤出文件类型的资源
        if (url.getProtocol().equalsIgnoreCase(FrameWorkConst.FILE_PROTOCOL)){
            classSet = new HashSet<Class<?>>();
            File packageDirectory = new File(url.getPath());
            extractClassFile(classSet, packageDirectory, packageName);
        }
        // TODO: 可以处理别的资源类型

        return classSet;
    }

    /**
     * 递归获取目标package里面的所有class文件(包括子package里的class文件)
     *
     * @param classSet 装载目标类的集合
     * @param packageDirectory    文件或者目录
     * @param packageName   包名
     * @return 类集合
     */
    private static void extractClassFile(Set<Class<?>> classSet, File packageDirectory, String packageName) {
        // 如果该packageDirectory是文件,则直接return不处理
        if(!packageDirectory.isDirectory()) return;
        // 1) 获得目录下的所有的File,并对其进行过滤:
        //    如果是目录,则还需要进一步的递归调用, 则加入到files数组, 然后之后遍历调用该函数进行穷举
        //    如果是文件,且是.class文件,那么可以直接对其进行路径切割获得全限定名称,从而通过反射创建Class对象并加入set中
        File[] files = packageDirectory.listFiles(new FileFilter() {
            @Override
            public boolean accept(File file) {
                if(file.isDirectory()) return true;
                // 如果是.class文件:
                String absoluteFilePath = file.getAbsolutePath();
                if(absoluteFilePath.endsWith(".class")) {
                    // 进行路径切割,获得全限定名称:
                    // 因为包名是.分割,而路径是/, 所以先替换
                    absoluteFilePath = absoluteFilePath.replace(File.separator,".");
                    // 从包名处截断, 一直到最后一个.的位置, 从而就可以得到包名.类名, 也就是全限定名称
                    String className = absoluteFilePath.substring(absoluteFilePath.indexOf(packageName),absoluteFilePath.lastIndexOf("."));
                    // 反射获得Class对象, 并把该Class对象加入到set中去
                    Class<?> clazz = loadClass(className);
                    classSet.add(clazz);
                }
                return false;
            }
        });
        // 2) 把files数组里面存的这些目录, 进一步递归调用
        if(!ValidationUtil.isEmpty(files)) {
            for (File file : files) {
                extractClassFile(classSet,file,packageName);
            }
        }
    }

    /**
     * 获得类加载器
     * @return
     */
    public static ClassLoader getClassLoader() {
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * 获取Class对象
     *
     * @param className class全名=package + 类名
     * @return Class
     */
    public static Class<?> loadClass(String className){
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            log.error(FrameWorkConst.LOAD_CLASS_ERROR_MSG, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 实例化class
     *
     * @param clazz Class
     * @param accessible   是否支持创建出私有class对象的实例
     * @return 类的实例化
     */
    public static Object newInstance(Class<?> clazz, boolean accessible){
        try {
            Constructor constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(accessible);
            return constructor.newInstance();
        } catch (Exception e) {
            log.error(FrameWorkConst.NEW_INSTANCE_ERROR_MSG, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 给target的field设置value值
     * @param field
     * @param target
     * @param value
     * @param accessible
     */
    public static void setField(Field field,Object target,Object value,boolean accessible) {
        try {
            field.setAccessible(accessible);
            field.set(target,value);
        } catch (Exception e) {
            log.error(FrameWorkConst.SET_FIELD_ERROR_MSG, e);
            throw new RuntimeException(e);
        }
    }
}
